package tp.pr5.mv;

import tp.pr5.mv.inout.*;
import tp.pr5.mv.traps.MVTrap;


public class CPU extends Observable<CPU.Observer> {
	
	/* La clase CPU consta de una memoria, una pila de operandos, un contador de programa, un programa, un booleano 
	 * que nos indica si la máquina está parada o no , una forma de leer y una forma de escribir. */
	
	private Memory memoryCPU; 
	private OperandStack stackCPU; 
	private ProgramCounter pc;
	private ProgramMV program;
	private boolean stop; 
	private InMethod inGeneral;
	private OutMethod outGeneral;
	private boolean aborted;

	public CPU() {
		
		/* El constructor de la clase crea una nueva memoria, una nueva pila de operandos, un nuevo programa
		 * y un nuevo contador de programa  e inicializa el booleano stop a false 
		 * (La máquina no está parada cuando creamos una CPU)*/
		
		memoryCPU = new Memory();
		stackCPU = new OperandStack();
		stop = false;
		pc = new ProgramCounter();
		program = new ProgramMV();
		inGeneral = new InKeyboard();      // Inicialización por defecto.
		outGeneral = new OutConsole();
		
		
	}
	
	public CPU( InMethod inMethod, OutMethod outMethod ) {     // Constructor que inicializa las dos interfaces gracias a dos parámetros.
		
		
		memoryCPU = new Memory();
		stackCPU = new OperandStack();
		stop = false;
		pc = new ProgramCounter();
		program = new ProgramMV();
		inGeneral = inMethod;
		outGeneral = outMethod;
		
	}
	
	public static interface Observer { // Interfaz que implementarán las vistas observadoras de la CPU
		
		public void onStartInstrExecution(Instruction ins);
		public void onEndInstrExecution(Instruction instr, Memory mem, OperandStack stack, ProgramCounter pc);
		public void onHalt();
		public void onReset(ProgramMV program);
		public void onStartRun();
		public void onEndRun(boolean aborted);
		public void onTrap(Instruction instr, String trapMessage);
		
		
	}
	
	public boolean isHalted() { // Método que devuelve un booleano indicando si la máquina está parada o no (es decir, si el 
		//contador de programa está en un rango válido y sino devuelve el valor de stop)
		
		if ( pc.getPC() < 0  ||  pc.getPC() >=  program.getNumInstrs() )   // PC va de 0 a instructionNumber-1.
			return true;
		else
			return stop;
		
	}
	
	public Memory getMemory() { // Método que devuelve la memoria de la CPU
		
		return memoryCPU;
		
	}
	
	public OperandStack getOperandStack() { // Método que devuelve la pila de operandos de la CPU
		
		return stackCPU;
		
	}
	
	public void resetMachine()     // Método que resetea la máquina (Reset del contador 
	//de programa y creación de una nueva memoria y pila).
	{
		
		pc.reset();
		memoryCPU.reset();
		stackCPU.reset();
		for ( Observer o: observers ) 
			o.onReset(program.copia());
		
	}
	
	public void loadProgram(ProgramMV program) {      // Método que carga un nuevo programa en la CPU.
		
		this.program = program; 
		resetMachine();
		
	}
	
	
	public boolean step() throws MVTrap {      // Método step de la CPU (Ejecución de una instrucción del programa introducido).
			
		if ( !isHalted() ) {    // Si la máquina no está parada...
				
			try {
			
				for ( Observer o: observers )
					o.onStartInstrExecution( getCurrentInstruction() );
			
				getCurrentInstruction().execute(stackCPU, memoryCPU , pc, inGeneral, outGeneral );    /* Aquí se ejecuta la instucción actual.
				Si la instrucción no se ha ejecutado bien lanza la MVTrap correspondiente. */
				
				for ( Observer o: observers )
					o.onEndInstrExecution(getCurrentInstruction(), memoryCPU, stackCPU, pc);
			
				pc.increment();	// Si la ejecución no tuvo errores, incrementamos el PC.
				
				if ( this.isHalted() )
					for ( Observer o: observers )
						o.onHalt();
				
				return true;
			}
			catch ( MVTrap ex ) {
				for ( Observer o: observers )
					o.onTrap(getCurrentInstruction(), ex.getMessage() );	
				throw ex;
			}
		}	
		else {
			for ( Observer o: observers )
				o.onHalt();
			return false; 
		}
		
	}

	public Instruction getCurrentInstruction() { // Método que nos da la instrucción actual.
		return program.getInstrAt(pc.getPC());
	}

	public void halt() { // Método para parar la máquina.
		this.stop = true;
		
		for ( Observer o: observers )
			o.onHalt();
	}

	public ProgramCounter getPCObject() { // Método que devuelve el PC (objeto) de la CPU.
		return pc;
	}
	
	public InMethod getInMethod() {      // Get de la implementación de InMethod.
		return this.inGeneral;
	}
	
	public OutMethod getOutMethod() {    // Get de la implementación de OutMethod.
		return this.outGeneral;
	}
	
	public void setInMethod( InMethod inMethod )      // Set de la implementación de InMethod.
	{
	    inGeneral = inMethod;
	}
	
	public void setOutMethod( OutMethod outMethod )   // Set de la implementación de InMethod.
	{
	    outGeneral = outMethod;
	}
	
	public ProgramMV getProgram() {
		return this.program;
	}
	
	public void setAborted(boolean b) {
		aborted = b;
	}
	public void run() { 
		
		try {
		run(0);
		} 
		catch (MVTrap e) {}
	}
	public void run(int delay) throws MVTrap {
		
		aborted = false;
		
		try {
			
			for ( Observer o: observers )
				o.onStartRun();
			
			while( !this.isHalted() && !aborted ){   // Mientras no esté parada y no nos hayan pedido parar...
				step();
				try {
					Thread.sleep(delay);
				} catch (InterruptedException e) {
				}
			}
			
		} 
		finally {
		for ( Observer o: observers )
				o.onEndRun(aborted);
		}
		
	}
	
	public void stopRun() {
		
	}
	
	public void step( int pasos ) throws MVTrap {
		
			int i = 0;
			
			while( !this.isHalted() && i < pasos ) {
				step();
				i++;
			}
	}

}
